package liquibase.datatype.core;

import java.util.Locale;
import java.util.regex.Pattern;
import liquibase.change.core.LoadDataChange;
import liquibase.database.Database;
import liquibase.database.core.DB2Database;
import liquibase.database.core.Db2zDatabase;
import liquibase.database.core.DerbyDatabase;
import liquibase.database.core.DmDatabase;
import liquibase.database.core.Firebird3Database;
import liquibase.database.core.FirebirdDatabase;
import liquibase.database.core.HsqlDatabase;
import liquibase.database.core.InformixDatabase;
import liquibase.database.core.MSSQLDatabase;
import liquibase.database.core.MySQLDatabase;
import liquibase.database.core.OracleDatabase;
import liquibase.database.core.PostgresDatabase;
import liquibase.database.core.SQLiteDatabase;
import liquibase.database.core.SybaseASADatabase;
import liquibase.database.core.SybaseDatabase;
import liquibase.datatype.DataTypeInfo;
import liquibase.datatype.DatabaseDataType;
import liquibase.datatype.LiquibaseDataType;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.statement.DatabaseFunction;
import liquibase.util.StringUtil;

@DataTypeInfo(name = "boolean", aliases = {"java.sql.Types.BOOLEAN", "java.lang.Boolean", "bit", "bool"}, minParameters = 0, maxParameters = 0, priority = LiquibaseDataType.PRIORITY_DEFAULT)
public class BooleanType extends LiquibaseDataType {

	@Override
	public DatabaseDataType toDatabaseDataType(Database database) {
		String originalDefinition = StringUtil.trimToEmpty(getRawDefinition());
		if ((database instanceof Firebird3Database)) {
			return new DatabaseDataType("BOOLEAN");
		}

		if ((database instanceof Db2zDatabase) || (database instanceof FirebirdDatabase)) {
			return new DatabaseDataType("SMALLINT");
		} else if (database instanceof MSSQLDatabase) {
			return new DatabaseDataType(database.escapeDataTypeName("bit"));
		} else if (database instanceof MySQLDatabase) {
			if (originalDefinition.toLowerCase(Locale.US).startsWith("bit")) {
				return new DatabaseDataType("BIT", getParameters());
			}
			return new DatabaseDataType("BIT", 1);
		} else if (database instanceof OracleDatabase) {
			return new DatabaseDataType("NUMBER", 1);
		} else if ((database instanceof SybaseASADatabase) || (database instanceof SybaseDatabase)) {
			return new DatabaseDataType("BIT");
		} else if (database instanceof DerbyDatabase) {
			if (((DerbyDatabase) database).supportsBooleanDataType()) {
				return new DatabaseDataType("BOOLEAN");
			} else {
				return new DatabaseDataType("SMALLINT");
			}
		} else if (database instanceof DB2Database) {
			if (((DB2Database) database).supportsBooleanDataType())
				return new DatabaseDataType("BOOLEAN");
			else
				return new DatabaseDataType("SMALLINT");
		} else if (database instanceof HsqlDatabase) {
			return new DatabaseDataType("BOOLEAN");
		} else if (database instanceof PostgresDatabase) {
			if (originalDefinition.toLowerCase(Locale.US).startsWith("bit")) {
				return new DatabaseDataType("BIT", getParameters());
			}
		} else if(database instanceof DmDatabase) {
			return new DatabaseDataType("bit");
		}

		return super.toDatabaseDataType(database);
	}

	@Override
	public String objectToSql(Object value, Database database) {
		if ((value == null) || "null".equals(value.toString().toLowerCase(Locale.US))) {
			return null;
		}

		String returnValue;
		if (value instanceof String) {
			value = ((String) value).replaceAll("'", "");
			if ("true".equals(((String) value).toLowerCase(Locale.US)) || "1".equals(value) || "b'1'".equals(((String) value).toLowerCase(Locale.US)) || "t".equals(((String) value).toLowerCase(Locale.US)) || ((String) value).toLowerCase(Locale.US).equals(this.getTrueBooleanValue(database).toLowerCase(Locale.US))) {
				returnValue = this.getTrueBooleanValue(database);
			} else if ("false".equals(((String) value).toLowerCase(Locale.US)) || "0".equals(value) || "b'0'".equals(
				((String) value).toLowerCase(Locale.US)) || "f".equals(((String) value).toLowerCase(Locale.US)) || ((String) value).toLowerCase(Locale.US).equals(this.getFalseBooleanValue(database).toLowerCase(Locale.US))) {
				returnValue = this.getFalseBooleanValue(database);
			} else if (database instanceof PostgresDatabase && Pattern.matches("b?([01])\\1*(::bit|::\"bit\")?", (String) value)) {
				returnValue = "b'"
					+ value.toString()
					.replace("b", "")
					.replace("\"", "")
					.replace("::it", "")
					+ "'::\"bit\"";
			} else {
				throw new UnexpectedLiquibaseException("Unknown boolean value: " + value);
			}
		} else if (value instanceof Long) {
			if (Long.valueOf(1).equals(value)) {
				returnValue = this.getTrueBooleanValue(database);
			} else {
				returnValue = this.getFalseBooleanValue(database);
			}
		} else if (value instanceof Number) {
			if (value.equals(1) || "1".equals(value.toString()) || "1.0".equals(value.toString())) {
				returnValue = this.getTrueBooleanValue(database);
			} else {
				returnValue = this.getFalseBooleanValue(database);
			}
		} else if (value instanceof DatabaseFunction) {
			return value.toString();
		} else if (value instanceof Boolean) {
			if (((Boolean) value)) {
				returnValue = this.getTrueBooleanValue(database);
			} else {
				returnValue = this.getFalseBooleanValue(database);
			}
		} else {
			throw new UnexpectedLiquibaseException("Cannot convert type "+value.getClass()+" to a boolean value");
		}

		return returnValue;
	}

	protected boolean isNumericBoolean(Database database) {
		if (database instanceof Firebird3Database) {
			return false;
		}
		if (database instanceof DerbyDatabase) {
			return !((DerbyDatabase) database).supportsBooleanDataType();
		} else if (database instanceof DB2Database) {
			return !((DB2Database) database).supportsBooleanDataType();
		}
		return (database instanceof Db2zDatabase) || (database instanceof FirebirdDatabase) || (database instanceof
			MSSQLDatabase) || (database instanceof MySQLDatabase) || (database instanceof OracleDatabase) ||
			(database instanceof SQLiteDatabase) || (database instanceof SybaseASADatabase) || (database instanceof
			SybaseDatabase) || (database instanceof DmDatabase);
	}

	/**
	 * The database-specific value to use for "false" "boolean" columns.
	 */
	public String getFalseBooleanValue(Database database) {
		if (isNumericBoolean(database)) {
			return "0";
		}
		if (database instanceof InformixDatabase) {
			return "'f'";
		}
		return "FALSE";
	}

	/**
	 * The database-specific value to use for "true" "boolean" columns.
	 */
	public String getTrueBooleanValue(Database database) {
		if (isNumericBoolean(database)) {
			return "1";
		}
		if (database instanceof InformixDatabase) {
			return "'t'";
		}
		return "TRUE";
	}

	@Override
	public LoadDataChange.LOAD_DATA_TYPE getLoadTypeName() {
		return LoadDataChange.LOAD_DATA_TYPE.BOOLEAN;
	}

}
